//*********************************************************
//
//    Copyright (c) Microsoft. All rights reserved.
//    This code is licensed under the Apache License, Version 2.0.
//    THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
//    ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
//    IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
//    PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
//
//*********************************************************

using System;
using System.Collections.Generic;
using System.Text;
using System.Data.Common;

namespace Microsoft.Research.DataLayer
{
    /// <summary>
    /// Back storage bound collection of items. This class is used to hold
    /// relationships between objects, track and update the backend storage
    /// properly.
    /// </summary>
    /// <typeparam name="O">Object type that owns the relationship</typeparam>
    /// <typeparam name="T">Object type that is owned in the relationship</typeparam>
    public abstract class SyncCollection<O, T> : ICollection<T>
        where O : IObject
        where T : IObject
    {
        protected Connection conn;
        O owner;
        string sourceKey;
        string sourceKeyRev;

        protected List<T> items = new List<T>();

        /// <summary>
        /// (Performance) Holds a guess of how many objects the collection holds, without
        /// fetching each object from the store. It is a common case to inspect o.Count and
        /// take decisions based on it before the need of the objects themselves.
        /// </summary>
        internal int countGuess;

        /// <summary>
        /// Delegate function to be called when data is needed (delay loaded)
        /// </summary>
        /// <param name="c">Valid connection to the data store</param>
        /// <returns>List of objects, that participate in the relationship,
        /// returned from the backend</returns>
        public delegate List<T> DelayLoadCallback(Connection c);

        /// <summary>
        /// Event fired when data is needed by the collection
        /// </summary>
        public event DelayLoadCallback OnDelayLoad;

        /// <summary>
        /// Handler for notifying that a new item was about to be inserted
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="newItem"></param>
        public delegate void BeforeInsertCallback(object sender, T newItem);

        /// <summary>
        /// Handler for notifying that a new item was inserted
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="newItem"></param>
        public delegate void AfterInsertCallback(object sender, T newItem);

        /// <summary>
        /// Handler for notifying that a new item was deleted
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="deletedItem"></param>
        /// <param name="success"></param>
        public delegate void AfterDeleteCallback(object sender, T deletedItem, bool success);

        /// <summary>
        /// Handler for notifying that a new batch was loaded
        /// </summary>
        /// <param name="sender"></param>
        public delegate void AfterLoadCallback(object sender);

        /// <summary>
        /// Fires on Add(T item) is about to be called
        /// </summary>
        public event BeforeInsertCallback OnBeforeInsert;

        /// <summary>
        /// Fires on Add(T item) is called
        /// </summary>
        public event AfterInsertCallback OnAfterInsert;

        /// <summary>
        /// Fires on Delete(T item) is called
        /// </summary>
        public event AfterDeleteCallback OnAfterDelete;

        /// <summary>
        /// Fires when Load(items) is called
        /// </summary>
        public event AfterLoadCallback OnAfterLoad;

        /// <summary>
        /// Ensures that the collection has real data before access is done, and retrieves
        /// it from the database on demand.
        /// </summary>
        protected virtual void CheckDelayLoad()
        {
            if (this.owner.ID.Equals(Guid.Empty))
            {
                throw new RecordNotFoundException("Operations on unsaved object is not supported");
            }
            if (countGuess != -1)
            {
                // Delay load this collection
                List<T> results = OnDelayLoad(conn);
                Load(results);
            }
        }

        /// <summary>
        /// Indexer. Allows to fetch items by index (e.g. Tags[i])
        /// </summary>
        /// <param name="index">Index of the object to be returned</param>
        /// <returns>Object in the position requested</returns>
        public T this[int index]
        {
            get
            {
                lock (this)
                {
                    CheckDelayLoad();
                    return items[index];
                }
            }
        }

        /// <summary>
        /// Fetches a new version of the relationship data from the storage
        /// </summary>
        public void Refresh()
        {
            this.items.Clear();

            // Find the new amount of data to chew next time
            Parameter countParam;

            if (this.SourceKeyRev != null)
                countParam = new Parameter(string.Format("{0}Count", this.SourceKeyRev), Parameter.ParamType.Int, Parameter.ParamDirection.Out);
            else
                countParam = new Parameter(string.Format("{0}Count", Connection.TypeStoreMap[typeof(T)]), Parameter.ParamType.Int, Parameter.ParamDirection.Out);

            List<Parameter> mydata = new List<Parameter>();
            mydata.Add(countParam);

            conn.Relation(Connection.Action.Count, owner, Connection.TypeStoreMap[typeof(T)], this.SourceKeyRev, ref mydata);

            if (countParam.Value == null)
                countGuess = -2;
            else
                countGuess = (countParam.Value == System.DBNull.Value ? 0 : (int)countParam.Value);
        }

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="owner">Object that owns the collection</param>
        /// <param name="c">Valid connection to the storage backend</param>
        protected SyncCollection(O owner, Connection c)
        {
            this.owner = owner;
            this.conn = c;
        }

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="owner">Object that owns the collection</param>
        /// <param name="c">Valid connection to the storage backend</param>
        /// <param name="key">Field to perform binding to</param>
        /// <param name="revKey">Field to perform reverse binding to</param>
        protected SyncCollection(O owner, string key, string revKey, Connection c)
        {
            this.owner = owner;
            this.conn = c;
            this.sourceKey = key;
            this.sourceKeyRev = revKey;
        }

        /// <summary>
        /// Load items into the collection
        /// </summary>
        /// <param name="items">Items to add to the collection</param>
        public void Load(List<T> items)
        {
            this.countGuess = -1;
            this.items.Clear();
            this.items.AddRange(items);

            if (OnAfterLoad != null)
                OnAfterLoad(this);
        }

        /// <summary>
        /// Object that owns the collection
        /// </summary>
        protected O Owner
        {
            get { return owner; }
        }

        /// <summary>
        /// Key to perform binding to
        /// </summary>
        protected string SourceKey
        {
            get { return sourceKey; }
        }

        /// <summary>
        /// Key to perform binding to
        /// </summary>
        protected string SourceKeyRev
        {
            get { return sourceKeyRev; }
        }

        #region ICollection<T> Members

        /// <summary>
        /// Add a new item to the relationship
        /// </summary>
        /// <param name="item">Item to build a relationship</param>
        public void Add(T item)
        {
            CheckDelayLoad();

            if (OnBeforeInsert != null)
                OnBeforeInsert(this, item);

            if (item is IExtraRelationData)
            {
                // Check for duplicates
                IExtraRelationData itemData = (IExtraRelationData)item;
                foreach (T itemHere in items)
                {
                    IExtraRelationData itemHereData = (IExtraRelationData)itemHere;
                    if (itemHereData.ChildID == itemData.ChildID)
                        throw new ObjectAlreadyExists("Object already exists in the collection", null);
                }
            }
            else
            {
                // Check for duplicates
                foreach (T itemHere in items)
                {
                    if (itemHere.ID == item.ID)
                        throw new ObjectAlreadyExists("Object already exists in the collection", null);
                }
            }

            // Store reference in memory
            items.Add(item);

            // Commit to database
            conn.Relation(Connection.Action.Add, owner, item);

            if (OnAfterInsert != null)
                OnAfterInsert(this, item);
        }

        /// <summary>
        /// Updates an item in the relationship
        /// </summary>
        /// <param name="item">Item to update relationship</param>
        public void Update(T item)
        {
            CheckDelayLoad();

            // Commit to database
            conn.Relation(Connection.Action.Update, owner, item);
        }

        /// <summary>
        /// Clear all relationship data and destroys all existing relationships
        /// </summary>
        public void Clear()
        {
            CheckDelayLoad();

            while (items.Count > 0)
            {
                T item = items[0];
                Remove(item);
            }
        }

        /// <summary>
        /// Checks if an object is present in the collection
        /// </summary>
        /// <param name="item">Item to check existence</param>
        /// <returns>true if item already exists</returns>
        public bool Contains(T item)
        {
            CheckDelayLoad();
            return items.Contains(item);
        }

        /// <summary>
        /// Copies data from the collection to another array.
        /// </summary>
        /// <param name="array">Target array</param>
        /// <param name="arrayIndex">Initial position on the array to copy items to</param>
        public void CopyTo(T[] array, int arrayIndex)
        {
            CheckDelayLoad();
            items.CopyTo(array, arrayIndex);
        }

        /// <summary>
        /// Returns the number of items that are present in the relationship
        /// </summary>
        public int Count
        {
            get
            {
                if (countGuess == -2)
                {
                    CheckDelayLoad();
                    countGuess = -1;
                }

                return countGuess != -1 ? countGuess : items.Count;
            }
        }

        /// <summary>
        /// Not readonly. Returns false.
        /// </summary>
        public bool IsReadOnly
        {
            get { return false; }
        }

        /// <summary>
        /// Remove a relationship between two objects.
        /// </summary>
        /// <param name="item">Item to remove</param>
        /// <returns>true if removal is successful</returns>
        public bool Remove(T item)
        {
            CheckDelayLoad();

            bool ret = items.Remove(item);

            if (!ret)
            {
                if (item is IExtraRelationData)
                {
                    // Check for duplicates
                    IExtraRelationData itemData = (IExtraRelationData)item;
                    foreach (T itemHere in items)
                    {
                        IExtraRelationData itemHereData = (IExtraRelationData)itemHere;
                        if (itemHereData.ChildID == itemData.ChildID)
                        {
                            // Found it
                            ret = items.Remove(itemHere);
                            break;
                        }
                    }
                }
                else
                {
                    // Try to find a similar item (if loaded by different means)
                    foreach (T itemHere in items)
                    {
                        if (itemHere.ID == item.ID)
                        {
                            // Found it
                            ret = items.Remove(itemHere);
                            break;
                        }
                    }
                }
            }

            if (ret)
            {
                // Remove reference
                conn.Relation(Connection.Action.Delete, owner, item);
            }

            if (OnAfterDelete != null)
                OnAfterDelete(this, item, ret);

            return ret;
        }

        #endregion

        /// <summary>
        /// Sorts the items
        /// </summary>
        /// <param name="comp"></param>
        public void Sort(Comparison<T> comp)
        {
            this.items.Sort(comp);
        }

        #region IEnumerable<T> Members

        /// <summary>
        /// Returns an enumerator object to navigate through the objects that 
        /// participate in a relationship with the owner
        /// </summary>
        /// <returns>Enumerator object</returns>
        public IEnumerator<T> GetEnumerator()
        {
            CheckDelayLoad();
            return items.GetEnumerator();
        }

        #endregion

        #region IEnumerable Members

        /// <summary>
        /// Return an enumerator to navigate through the collection items
        /// </summary>
        /// <returns>Enumerator object</returns>
        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            CheckDelayLoad();
            return GetEnumerator();
        }

        #endregion

        /// <summary>
        /// Builds the collection from data fetched from data store. Reader contains
        /// all items read in a single request (usually).
        /// </summary>
        /// <param name="reader">Object that holds items data</param>
        protected void LoadCollectionFromReader(DbDataReader reader)
        {
#if false
            countField = (Connection.Action)Enum.Parse(typeof(Connection.Action),
                string.Format("{0}Count", typeof(T).Name));
            int index = reader.GetOrdinal(countField.ToString());
#else
            int index;

            if (this.SourceKeyRev != null)
                index = reader.GetOrdinal(string.Format("{0}Count", this.SourceKeyRev));
            else
                index = reader.GetOrdinal(string.Format("{0}Count", typeof(T).Name));
#endif
            int objCount = -2;
            if (!reader.IsDBNull(index))
            {
                objCount = reader.GetInt32(index);
            }

            this.countGuess = objCount;
        }
    }

    public abstract class SyncCollectionSelf<O, T> : SyncCollection<O, T>
        where O : IObject
        where T : IObject
    {
        string fieldName;

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="owner">Object that owns the collection</param>
        /// <param name="c">Valid connection to the storage backend</param>
        /// <param name="key">Field to perform binding to</param>
        /// <param name="revKey">Field to perform reverse binding to</param>
        protected SyncCollectionSelf(O owner, string key, string revKey, string fieldName, Connection c)
            : base (owner, key, revKey, c)
        {
            this.fieldName = fieldName;
        }

        #region IEnumerable<T> Members

        /// <summary>
        /// Returns an enumerator object to navigate through the objects that 
        /// participate in a relationship with the owner
        /// </summary>
        /// <returns>Enumerator object</returns>
        public new IEnumerator<T> GetEnumerator()
        {
            CheckDelayLoad();
            return items.GetEnumerator();
        }

        /// <summary>
        /// Delegate function to be called when data is needed (delay loaded)
        /// </summary>
        /// <param name="c">Valid connection to the data store</param>
        /// <returns>List of objects, that participate in the relationship,
        /// returned from the backend</returns>
        public delegate List<T> DelayLoadCallback2(Connection c, string fieldName);

        /// <summary>
        /// Event fired when data is needed by the collection
        /// </summary>
        public event DelayLoadCallback2 OnDelayLoad2;
        
        /// <summary>
        /// Ensures that the collection has real data before access is done, and retrieves
        /// it from the database on demand.
        /// </summary>
        protected override void CheckDelayLoad()
        {
            if (countGuess != -1)
            {
                // Delay load this collection
                List<T> results = OnDelayLoad2(conn, this.fieldName);
                Load(results);
            }
        }
        
        #endregion
    }

}
